-
Notifications
You must be signed in to change notification settings - Fork 167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PROD-30967: Hux implementation #4118
Conversation
aaee41f
to
afb617b
Compare
use Drupal\social_post\Service\SocialPostHelperInterface; | ||
|
||
|
||
final class SocialPostFormHooks { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially wanted to have 1 class per hook replacement for better readability and maintenance but it will break the OS rule about not extra naming what we should follow. If I have 1 class per hook replacement the className would have the same name as the method (kind of).
IE: replacement of social_post_form_post_form_alter
ClassName SocialPostFormPostFormAlterHook
, method called SocialPostFormPostFormAlterHook
.
I'm really up for suggestions here if we group hooks together or not in a class? (my preference is not) And then how do we name the class and the method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first instinct would be to split it in different classes. There will likely be cases where we it makes more sense to group them, e.g. when multiple hooks execute the same underlying code.
And I would ignore the rule about extra naming. Maybe it looks a bit weird, but while reading it you know what it does 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I look at what the hook does in this case then it's preparing the post form to be personalised to the user and to the context?
What about Drupal\social_post\Hooks\PostFormPersonalisation
as fully qualified name? It indicates it's doing something to the Post form and it's personalising. "Social" is omitted because that's included in the namespace already which IDEs also include in their class search.
modules/social_features/social_post/src/Hooks/SocialPostFormHooks.php
Outdated
Show resolved
Hide resolved
modules/social_features/social_post/src/Hooks/SocialPostFormHooks.php
Outdated
Show resolved
Hide resolved
modules/social_features/social_post/src/Hooks/SocialPostFormHooks.php
Outdated
Show resolved
Hide resolved
e3f05d8
to
8e34027
Compare
modules/social_features/social_group/src/CurrentGroupService.php
Outdated
Show resolved
Hide resolved
164411f
to
77d4b2b
Compare
0e599b9
to
b4209ec
Compare
Tugboat has finished building the preview for this pull request! Link: Dashboard: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@denis-getopensocial thanks a lot for the hard work!
I looked through all the code and I think this is definitely much better than the form_alters
we've applied before. It's great to see additional unit testing is made possible and easier this way!
I left a few comments on files and your own remarks.
...es/social_features/social_post/modules/social_post_photo/src/Plugin/Block/PostPhotoBlock.php
Outdated
Show resolved
Hide resolved
...al_features/social_post/modules/social_post_photo/src/Plugin/Block/PostPhotoProfileBlock.php
Outdated
Show resolved
Hide resolved
use Drupal\social_post\Service\SocialPostHelperInterface; | ||
|
||
|
||
final class SocialPostFormHooks { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first instinct would be to split it in different classes. There will likely be cases where we it makes more sense to group them, e.g. when multiple hooks execute the same underlying code.
And I would ignore the rule about extra naming. Maybe it looks a bit weird, but while reading it you know what it does 🤷
modules/social_features/social_post/src/Hooks/SocialPostFormHooks.php
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry this review took a while :D While I was writing this review, Bram also commented on some things so there may be some overlap :)
I realise some of the things I'm reviewing might be debatable here, which is also why I think this PR might even be split into 3 PRs:
- Add the hux dependency (that's already approved and non-controversial)
- Rewrite the way to get the current group (we have a few smaller points there) which might even be used by the current hook implementation. It comes to light because we're using Hux but it's an improvement we may still want to make even if we wouldn't use Hux, so it's really an independent change
- Rewrite the hook to Hux which is where the larger discussion is currently ongoing
I'm trying to get other developers to also think more about "What are the smallest independent changes that we can make".
social_group.current_group: | ||
class: Drupal\social_group\CurrentGroupService | ||
arguments: | ||
- '@entity_type.manager' | ||
- '@group.group_route_context' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coding guideline 3.6 Use service autowiring and alias interfaces to concrete ID'd service definitions suggests some changes here.
Preferably the service itself would be an interface that would be used for type-hinting, but I don't think that makes sense here.
social_group.current_group: | |
class: Drupal\social_group\CurrentGroupService | |
arguments: | |
- '@entity_type.manager' | |
- '@group.group_route_context' | |
Drupal\social_group\CurrentGroupService: | |
class: Drupal\social_group\CurrentGroupService | |
autowire: true | |
social_group.current_group: '@Drupal\social_group\CurrentGroupService' |
(Drupal Core sets the alias the other way around but I don't really understand why, since Symfony's preferrable way is to use interfaces rather than distinct names).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can create an Interface for every things we create but in that case I dont see the point. If i s required I'm happy to comply otherwise i think it s unnecessary artefact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agreed in my message that an interface may not be necessary here. We should still make sure that we use autowiring and define our services in a way so that this class can be used by another service that uses autowiring. That's what my suggested code snippet achieves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
ContextProviderInterface $group_route_context, | ||
) { | ||
$this->entityTypeManager = $entity_type_manager; | ||
$this->groupRouteContext = $group_route_context; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing the context system in this way was new to me and I was wondering whether we should be calling into the group service directly like this.
While figuring out how the system works I came about the context.repository
system which we can also use by providing the service name and context (@group.group_route_context:group
) to its getRuntimeContexts
methods. The benefit of using that system is that it will also statically cache the context value so it's not recomputed in case the method is called multiple times.
What do you think of moving this over to Drupal's context repository rather than calling a system directly? It also means that if we want to conditionally pull from multiple contexts in the future we don't need another service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbh i was looking at how to, and the only way that was working was that.
I can try your approach.
modules/social_features/social_group/src/CurrentGroupService.php
Outdated
Show resolved
Hide resolved
return NULL; | ||
} | ||
|
||
$group = $runtime_context['group']->getContextData()->getValue(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the context class provides you with a shorter way to do this that doesn't require traversing the data itself, by calling getContextValue
directly on the context. Since that throws an exception if a required context isn't present (and group is required) we should check for hasContextValue
first though.
$group = $runtime_context['group']->getContextData()->getValue(); | |
if (!$runtime_context['group']->hasContextValue()) { | |
return NULL; | |
} | |
return $runtime_context['group']->getContextValue(); |
Since PHPStan likely can't entirely follow the types we can add an assert
to help show the invariant.
$group = $runtime_context['group']->getContextData()->getValue(); | |
if (!$runtime_context['group']->hasContextValue()) { | |
return NULL; | |
} | |
$group = $runtime_context['group']->getContextValue(); | |
assert($group instanceof GroupInterface, "The group context resolver returned a context value that is not a GroupInterface instance which violates the services contract".); | |
return $group; |
return $group; | ||
} | ||
|
||
if (is_int($group) === TRUE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's one stylistic thing I disagree with and one functional issue in the code:
Stylistically: I'm unsure why the === TRUE
is added here since that's the default for if-statements. Especially since is_int
returns an exact boolean and no truthy values. if (is_int($group))
is less complex to view and equivalent so it should be preferred.
Functionally: This code will never be true. The group route service resolves the context and returns \Drupal\group\Entity\GroupInterface|null
as type for the context service.
In the old code we would get the parameter from the route directly. That can be an integer in the case of views (which doesn't configure upcasting for its arguments). However, the group implementation converts that to NULL
due to the following line of code in GroupRouteContextTrait::getGroupFromRoute
: if (($group = $route_match->getParameter('group')) && $group instanceof GroupInterface) {
.
Fixing that on our side is slightly more difficult because it means that things that are used in views (Actions/Blocks/Forms) should use the plugin context system or the form context system rather than getting the context from the route. That way they get the instance of the context that Views itself has converted into an entity. So basically anything that might need this service for this code-path, shouldn't use this service.
modules/social_features/social_post/src/Hooks/SocialPostFormHooks.php
Outdated
Show resolved
Hide resolved
modules/social_features/social_post/src/Hooks/SocialPostFormHooks.php
Outdated
Show resolved
Hide resolved
$form_object = $form_state->getFormObject(); | ||
|
||
if ($form_object instanceof ContentEntityForm === FALSE) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if $form_object
is not a ContentEntityForm
then we have an implementation error right? So we probably want to make this an assert
if we expect to catch this in testing or throw an actual exception if we think this might happen at run-time but shows an implementation error. The alternative would likely be a subtle and hard to debug bug (the post form not being altered) due to something that's wrong in an unrelated system (the post form no longer being a content entity form).
This if-statement also causes $content
to be calculated but never used, so that statement should likely be after this guard clause.
|
||
if ($form_object->getEntity()->isNew() | ||
&& $content !== NULL) { | ||
$form['current_user_image'] = $content; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$content
is only ever used here so I would suggest inlining the variable. That has the advantage that if this is a post being edited we don't need to calculate $content
.
// Default value. | ||
$form = $this->setFormTitleAndPlaceholder($form, t('Say something to the Community')); | ||
|
||
if ($form_state->get('currentGroup') !== NULL) { | ||
$form = $this->setFormTitleAndPlaceholder($form, t('Say something to the group')); | ||
} | ||
|
||
// $user_profile = $this->routeMatch->getParameter('user'); | ||
$user_profile = $form_state->get('recipientUser'); | ||
if ($user_profile !== NULL | ||
&& $user_profile->id() !== $this->currentUser->id()) { | ||
$form = $this->setFormTitleAndPlaceholder($form, t('Leave a message to @name', [ | ||
'@name' => $user_profile->getDisplayName(), | ||
])); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we can re-arrange this logic to make sure we don't set the default value and then overwrite it but only set the default value if we haven't picked another value. In this case we know setFormTitleAndPlaceholder
is a very light-weight function, but since it's abstracted, we really shouldn't need to know this and the function might be calculating pi or fetching expensive data from the database unnecessarily. We should not be afraid to use else
if it makes sense :)
c60df44
to
cf40d65
Compare
165685b
to
e62b1d6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this Denis! Code looks good like this to me.
Once the pipelines are green this can be merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shout out to you @denis-getopensocial for moving this forward.
Not an easy process, but I do think this is really good! Congratulations 👏
6da5d3b
to
9ee7fbb
Compare
9ee7fbb
to
ea038d7
Compare
@@ -11,3 +11,17 @@ services: | |||
social_post.helper: | |||
class: Drupal\social_post\Service\SocialPostHelper | |||
arguments: ['@entity_type.manager', '@current_user'] | |||
|
|||
#hooks replacement |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the proper auto-wiring should looks like this (credit Alexander)
#hooks replacement
# use autowiring for arguments for `SocialPostFormHooks`
social_post.form.hooks:
class: Drupal\social_post\Hooks\SocialPostFormHooks
autowire: true
tags:
- { name: hooks }
# make `SocialPostFormHooks` injectable in another service (e.g. for decoration)
Drupal\social_post\Hooks\SocialPostFormHooks: '@social_post.form.hooks'
Problem
As outlined in our decision record, we have identified the need to integrate Hux instead of relying on traditional hooks for managing interactions and events within our software. The use of hooks, while functional, has led to limitations in scalability, maintainability, and flexibility.
Solution
We will migrate from the current hook-based implementation to Hux. The Hux library offers a more modern and modular approach for handling events, making the system easier to maintain and extend in the long term.
Blog post on how to: https://www.previousnext.com.au/blog/hux-alternative-to-hooks
Drupal seems to go the same way: https://www.drupal.org/node/3442349
Issue tracker
https://getopensocial.atlassian.net/browse/PROD-30967
Theme issue tracker
How to test
Screenshots
Release notes
Change Record
Creation of a
current group service
to fetch the current group service from the context.Translations